package edu.ufl.cise.sanitas.client.proxy;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.zip.GZIPInputStream;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import edu.ufl.cise.sanitas.types.comm.ConnectionStatus;
import edu.ufl.cise.sanitas.types.comm.FetchAllNewsfeedRequest;
import edu.ufl.cise.sanitas.types.comm.PortComfirmationPacket;
import edu.ufl.cise.sanitas.types.comm.ProxyTimeoutResponse;
import edu.ufl.cise.sanitas.types.comm.Request;
import edu.ufl.cise.sanitas.types.comm.Response;
import edu.ufl.cise.sanitas.types.comm.SyncPostsRequest;
import edu.ufl.cise.sanitas.types.comm.UpdateUserConditionsRequest;
import edu.ufl.cise.sanitas.types.data.Condition;
import edu.ufl.cise.sanitas.types.data.Feed;
import edu.ufl.cise.sanitas.types.data.Newsfeed;
import edu.ufl.cise.sanitas.types.data.User;

/**
 * @author Xiaoyang
 * 
 */
public class Proxy {

	public static final int MESSAGECODE_NEWFEED = 0;
	public static final int MESSAGECODE_FETCHALL = 0;

	public static Proxy proxy;

	private static final String LOG_TAG = "sanitas.client.proxy.Proxy";

	private Handler updateNewsfeedHandler;

	public void setUpdateNewsfeedHandler(Handler updateNewsfeedHandler) {
		this.updateNewsfeedHandler = updateNewsfeedHandler;
	}

	public static Proxy getInstance(Context ctx, Handler newFeedHandler) {
		proxy = new Proxy(ctx, newFeedHandler);
		return proxy;
	}

	public static Proxy getInstance() {
		return proxy;
	}

	private Proxy(Context ctx, final Handler newFeedHandler) {
		this.mCtx = ctx;

		new Thread(new Runnable() {

			@Override
			public void run() {
				DatagramSocket socket;
				try {
					socket = new DatagramSocket();

					Log.i(LOG_TAG, "Listener Socket Created! Information: \n"
							+ "IP: " + socket.getLocalSocketAddress()
							+ "\nPort: " + socket.getPort() + "\nLocalPort: "
							+ socket.getLocalPort());
					ByteArrayOutputStream baos = new ByteArrayOutputStream();
					ObjectOutputStream oos = new ObjectOutputStream(baos);
					oos.writeObject(new PortComfirmationPacket());
					oos.close();

					byte[] data = baos.toByteArray();
					
					baos.close();

					socket.send(new DatagramPacket(data, data.length, InetAddress.getByName(mServerAddr), mPort));
					Log
							.i(LOG_TAG,
									"Listener Socket comfirmation packet has been sent!");
					socket.setSoTimeout(0);

					byte[] buffer = new byte[10240];
					DatagramPacket packet = new DatagramPacket(buffer, 10240);
					while (true) {
						socket.receive(packet);

						Log.i(LOG_TAG, "Broadcast packet is received!");

						byte[] info = packet.getData();
						
						ByteArrayInputStream bais = new ByteArrayInputStream(info);
						ObjectInputStream ois = new ObjectInputStream(bais);
						
						Feed feed = (Feed) ois.readObject();
						
						ois.close();
						bais.close();

						Message msg = new Message();
						msg.obj = feed;
						msg.what = MESSAGECODE_NEWFEED;
						if (newFeedHandler != null) {
							newFeedHandler.sendMessage(msg);
						}

					}
				} catch (SocketException e) {
					e.printStackTrace();
				} catch (UnknownHostException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				}

				Log.i(LOG_TAG, "Broadcast listener thread exited!");
			}

		}).start();
	}

	// User data fields
	private static Condition[] conditionList;
	private static boolean updateConditionList;
	private static User user;
	// Environment-related fields
	private final Context mCtx;

	// Communication-related fields
	private static final String mServerAddr = "10.128.83.102";
	private static final int mPort = 5577;

	public void insertFeedToFeeds(Feed newFeed) {
		ContentValues contentValues = new ContentValues();
		contentValues.put(KEY_FEEDS_USERNAME, newFeed.getUserName());
		contentValues.put(KEY_FEEDS_CONTENT, newFeed.getContent());
		contentValues.put(KEY_FEEDS_TIME, newFeed.getTimeStamp());
		contentValues.put(KEY_FEEDS_THEME, newFeed.getTheme());
		mDb.insert(DATABASE_FEED_TABLE, null, contentValues);
	}

	// Database-related fields
	private DatabaseHelper mDbHelper;
	private SQLiteDatabase mDb;
	private static final String DATABASE_NAME = "sanitasclient";
	private static final String DATABASE_FEED_TABLE = "feeds";
	private static final String DATABASE_SYNCFEED_TABLE = "syncfeeds";
	private static final int DATABASE_VERSION = 3;

	public static final String KEY_FEEDS_ID = "_id";
	public static final String KEY_FEEDS_USERNAME = "username";
	public static final String KEY_FEEDS_CONTENT = "content";
	public static final String KEY_FEEDS_TIME = "time";
	public static final String KEY_FEEDS_THEME = "theme";
	private static final String DATABASE_CREATE_FEEDS = "create table feeds "
			+ "(_id integer primary key autoincrement, username text not null, content text not null, time text not null, theme text not null);";
	private static final String DATABASE_CREATE_SYNCFEEDS = "create table syncfeeds"
			+ "(_id integer primary key autoincrement, content text not null, time text not null, theme text not null);";
	public static final String KEY_DAILYLOGS_ID = "_id";
	public static final String KEY_DAILYLOGS_FOOD = "food";
	public static final String KEY_DAILYLOGS_SPORT = "sport";
	public static final String KEY_DAILYLOGS_WEIGHT = "weight";
	public static final String KEY_DAILYLOGS_SLEEPTIME = "sleeptime";
	public static final String KEY_DAILYLOGS_SLEEPSOUND = "sleepsound";
	public static final String KEY_DAILYLOGS_SLEEPLIGHT = "sleeplight";
	private static final String DATABASE_CREATE_DAILYLOGS = "create table dailylogs "
			+ "(_id integer primary key autoincrement, food integer, sport integer, weight integer, sleeptime integer, sleeplight integer, sleepsound integer);";
	private static final String DATABASE_CREATE_SYNCDAILYLOGS = "create table syncdailylogs "
			+ "(_id integer primary key autoincrement, food integer, sport integer, weight integer, sleeptime integer, sleeplight integer, sleepsound integer);";

	private static class DatabaseHelper extends SQLiteOpenHelper {

		public DatabaseHelper(Context context) {
			super(context, DATABASE_NAME, null, DATABASE_VERSION);
		}

		@Override
		public void onCreate(SQLiteDatabase db) {
			db.execSQL(DATABASE_CREATE_FEEDS);
			db.execSQL(DATABASE_CREATE_SYNCFEEDS);
			db.execSQL(DATABASE_CREATE_DAILYLOGS);
			db.execSQL(DATABASE_CREATE_SYNCDAILYLOGS);
		}

		@Override
		public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
			db.execSQL("DROP TABLE IF EXISTS feeds");
			db.execSQL("DROP TABLE IF EXISTS syncfeeds");
			db.execSQL("DROP TABLE IF EXISTS dailylogs");
			db.execSQL("DROP TABLE IF EXISTS syncdailylogs");

			onCreate(db);
		}
	}

	// Database-related methods
	public Proxy open() {
		mDbHelper = new DatabaseHelper(mCtx);
		mDb = mDbHelper.getWritableDatabase();
		return this;
	}

	public void updateAllNewsfeed(Newsfeed newsfeed) {
		mDb.delete(DATABASE_FEED_TABLE, null, null);

		Feed[] feedList = newsfeed.getFeedList();
		for (Feed f : feedList) {
			ContentValues contentValues = new ContentValues();
			contentValues.put(KEY_FEEDS_USERNAME, f.getUserName());
			contentValues.put(KEY_FEEDS_CONTENT, f.getContent());
			contentValues.put(KEY_FEEDS_TIME, f.getTimeStamp());
			contentValues.put(KEY_FEEDS_THEME, f.getTheme());
			mDb.insert(DATABASE_FEED_TABLE, null, contentValues);
		}
	}

	public void close() {
		mDb.close();
	}

	public Cursor fetchAllFeeds() {
		return mDb.query(DATABASE_FEED_TABLE, new String[] { KEY_FEEDS_ID,
				KEY_FEEDS_USERNAME, KEY_FEEDS_TIME, KEY_FEEDS_CONTENT,
				KEY_FEEDS_THEME }, null, null, null, null, KEY_FEEDS_TIME
				+ " DESC");
	}

	public void requestNewsfeed() {
		FetchAllNewsfeedRequest request = new FetchAllNewsfeedRequest(this
				.getConnectionStatus(), user.getUserId());
		sendRequest(request, this.updateNewsfeedHandler, MESSAGECODE_FETCHALL);
	}

	// Connection-related methods
	public ConnectionStatus getConnectionStatus() {
		return ConnectionStatus.WEAK;
	}

	public void sendRequest(final Request request, final Handler handler,
			final int messageCode) {

		new Thread(new Runnable() {

			@Override
			public void run() {
				Response response = sendPacketAndWait(request);

				Message msg = new Message();
				msg.obj = response;
				msg.what = messageCode;
				
				Request r = syncPosts();
				if (r != null) sendPacketAndWait(r);
				r = syncConditions();
				if (r != null) {
					response = sendPacketAndWait(r);
					if (response.getStatus() == Response.Status.SUCCESS) {
						updateConditionList = false;
					}
				}
				
				handler.sendMessage(msg);
			}

		}).start();

	}

	private Response sendPacketAndWait(Request request) {

		DatagramSocket socket = null;
		try {
			InetAddress address = InetAddress.getByName(mServerAddr);
			socket = new DatagramSocket();
			socket.setSoTimeout(3000);

			Log.i(LOG_TAG, "New Request Socket Created, Information:" + "\nIP: "
					+ socket.getInetAddress() + "\nPort: " + socket.getPort()
					+ "\nLocalPort: " + socket.getLocalPort());

			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(baos);
			oos.writeObject(request);
			
			byte[] data = baos.toByteArray();
			
			oos.close();
			baos.close();

			DatagramPacket requestPacket = new DatagramPacket(data,
					data.length, address, mPort);

			Log.i(LOG_TAG, "Request Packet " + request.getType() + " is sending...");
			socket.send(requestPacket);
			Log.i(LOG_TAG, "Request Packet " + request.getType() + " has been sent...");

			byte[] buf = new byte[10240];

			DatagramPacket bufPacket = new DatagramPacket(buf, 10240);
			Log.i(LOG_TAG, "Waiting for Response for " + request.getType() + " Request");
			socket.receive(bufPacket);
			Log.i(LOG_TAG, "Response has been received!");

			Response response = null;
			ByteArrayInputStream bais = new ByteArrayInputStream(bufPacket.getData());
			
			if (request.getConnectionStatus() == ConnectionStatus.WEAK) {
				Log.i(LOG_TAG, "WEAK Connection Detected. Start decompressing...");
				GZIPInputStream gzin=new GZIPInputStream(bais);
				ObjectInputStream in = new ObjectInputStream(gzin); 
				response =(Response) in.readObject();
				gzin.close();
				in.close();
				Log.i(LOG_TAG, "Decompression completed!");
			}
			else {
				Log.i(LOG_TAG, "STRONG Connection Detected. Direct unpack response.");
				ObjectInputStream ois = new ObjectInputStream(bais);
				response = (Response) ois.readObject();
				
				ois.close();
				Log.i(LOG_TAG, "Unpack completed.");
				
			}
			bais.close();
			
			socket.close();
			Log.i(LOG_TAG, "Temporary socket closed!");

			return response;

		} catch (SocketTimeoutException e) {
			Log.i(LOG_TAG, "Waiting for " + request.getType() + " response Time Out!");
			socket.close();
			Log.i(LOG_TAG, "Temporary socket closed!");
			Response response = new ProxyTimeoutResponse();
			Log.i(LOG_TAG, "ProxyTimeoutResponse Generated!");
			return response;
		} catch (SocketException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		return null;
	}
	
	private Request syncPosts() {
		Cursor c =  mDb.query(DATABASE_SYNCFEED_TABLE, new String[] { KEY_FEEDS_ID,
				KEY_FEEDS_TIME, KEY_FEEDS_CONTENT,
				KEY_FEEDS_THEME }, null, null, null, null, null);
		if (c.getCount() == 0) return null;
		
		Feed[] feedList = new Feed[c.getCount()];
		int count = -1; 
		
		c.moveToFirst();
		do {
			++count;
			feedList[count] = new Feed(c.getString(2), user.getName(), c.getString(3), c.getString(1));
		} while (count != c.getCount());
		c.close();
		return new SyncPostsRequest(proxy.getConnectionStatus(), feedList, user.getName(), user.getPassword());
		
	}
	
	private Request syncConditions() {
		if (updateConditionList) 
			return new UpdateUserConditionsRequest(proxy.getConnectionStatus(), getConditionStringList(), user.getName(), user.getPassword());
		else return null;
	}

	public void storePostLocally(Feed feed) {
		ContentValues contentValues = new ContentValues();
		contentValues.put("KEY_FEEDS_CONTENT", feed.getContent());
		contentValues.put("KEY_FEEDS_THEME", feed.getTheme());
		contentValues.put("KEY_FEEDS_TIME", feed.getTimeStamp());
		mDb.insert(DATABASE_SYNCFEED_TABLE, null, contentValues);
	}

	public static void setConditionList(Condition[] conditionList) {
		Proxy.conditionList = conditionList;
	}
	
	public static void setConditionList(String[] condList) {
		Condition[] array = new Condition[condList.length];
		for (int i = 0; i != condList.length; ++i) {
			array[i] = new Condition(condList[i]);
		}
		Proxy.conditionList = array;
	}

	public static Condition[] getConditionList() {
		return conditionList;
	}
	
	public static String[] getConditionStringList() {
		String[] array = new String[conditionList.length];
		for (int i = 0; i != conditionList.length; ++i) {
			array[i] = conditionList[i].getConditionName();
		}
		return array;
	}

	public static User getUser() {
		return user;
	}

	public static void setUser(User user) {
		Proxy.user = user;
	}

	public static void setUpdateConditionList(boolean updateConditionList) {
		Proxy.updateConditionList = updateConditionList;
	}

}
